package com.netx.data;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import com.netx.generics.basic.ValidationException;
import com.netx.generics.sql.Row;
import com.netx.generics.sql.Table;
import com.netx.generics.util.Strings;


public abstract class Entity {

	private Connection _connection;
	private EntityMetaData _metadata;

	protected Entity() {
		_connection = null;
		_metadata = null;
	}

	public String toString() {
		_checkInited();
		return getMetaData().getName();
	}

	public final EntityMetaData getMetaData() {
		_checkInited();
		return _metadata;
	}

	public final RelationMetaData getRelationMetaData() {
		_checkInited();
		return _metadata.getRelationMetaData();
	}

	protected final Connection getConnection() {
		_checkInited();
		return _connection;
	}

	protected final Row select(String fields, long primaryKey) throws DatabaseException {
		_checkInited();
		Checks.checkKey(primaryKey);
		Checks.checkString(fields);
		Table data = _connection.select(fields+" FROM "+getMetaData().getName()+" WHERE "+getMetaData().getPrimaryKey()[0].getName()+"="+primaryKey);
		if(data.isEmpty()) {
			return null;
		}
		else {
			return data.getRow(0);
		}
	}

	protected final Row select(String fields, ValueList primaryKey) throws DatabaseException {
		_checkInited();
		Checks.checkString(fields);
		Checks.checkNull(primaryKey);
		Checks.checkEmptyValueList(primaryKey);
		String where = _primaryKeyToSQLWhere(primaryKey);
		// ok, build the query:
		StringBuilder sb = new StringBuilder();
		sb.append("SELECT ");
		sb.append(fields);
		sb.append(" FROM ");
		sb.append(getMetaData().getName());
		sb.append(" WHERE ");
		sb.append(where);
		Table data = _connection.select(sb.toString());
		if(data.isEmpty()) {
			return null;
		}
		else {
			return data.getRow(0);
		}
	}

	protected final Table select(String fields, String query) throws DatabaseException {
		_checkInited();
		Checks.checkString(fields);
		StringBuilder queryBuf = new StringBuilder(fields+" FROM "+getMetaData().getName());
		if(!Strings.isEmpty(query)) {
			String query2 = query.trim().toUpperCase();
			if(query2.startsWith("ORDER BY") || query2.startsWith("GROUP BY")) {
				queryBuf.append(" "+query);
			}
			else {
				queryBuf.append(" WHERE "+query);
			}
		}
		return _connection.select(queryBuf.toString());
	}
	
	protected final Table select(String fields) throws DatabaseException {
		return select(fields, (String)null);
	}
	
	protected final long insert(ValueList values) throws DatabaseException {
		_checkInited();
		Checks.checkNull(values);
		Checks.checkEmptyValueList(values);
		return _insert(_metadata, values, getMetaData().autoGeneratedKeys());
	}

	protected final int update(long primaryKey, ValueList values) throws DatabaseException {
		_checkInited();
		Checks.checkKey(primaryKey);
		Checks.checkNull(values);
		Checks.checkEmptyValueList(values);
		return _update(_metadata, values, _metadata.getPrimaryKey()[0].getName()+"="+primaryKey);
	}

	protected final int update(ValueList primaryKey, ValueList values) throws DatabaseException {
		_checkInited();
		Checks.checkNull(primaryKey);
		Checks.checkEmptyValueList(primaryKey);
		Checks.checkNull(values);
		Checks.checkEmptyValueList(values);
		String where = _primaryKeyToSQLWhere(primaryKey);
		return _update(_metadata, values, where);
	}

	protected final int update(ValueList values, String query) throws DatabaseException {
		_checkInited();
		Checks.checkNull(values);
		Checks.checkEmptyValueList(values);
		return _update(_metadata, values, query);
	}

	protected final int delete(long primaryKey) throws DatabaseException {
		_checkInited();
		Checks.checkKey(primaryKey);
		return _delete(_metadata, _metadata.getPrimaryKey()[0].getName()+"="+primaryKey);
	}

	protected final int delete(String query) throws DatabaseException {
		_checkInited();
		Checks.checkString(query);
		return _delete(_metadata, query);
	}

	protected final void insertRelation(long entityKey, long relationKey, ValueList values) throws DatabaseException {
		_checkInited();
		_checkRelation(_metadata.getRelationMetaData());
		Checks.checkKey(entityKey);
		Checks.checkKey(relationKey);
		Checks.checkNull(values);
		MetaData related = _metadata.getRelationMetaData().getRelatedEntity();
		values.addValue(_metadata.getPrimaryKey()[0].getName(), new Long(entityKey));
		values.addValue(related.getPrimaryKey()[0].getName(), new Long(relationKey));
		_insert(_metadata.getRelationMetaData(), values, false);
	}

	protected final void insertRelation(long entityKey, long relationKey) throws DatabaseException {
		insertRelation(entityKey, relationKey, new ValueList());
	}

	protected final int updateRelation(long entityKey, long relationKey, ValueList values) throws DatabaseException {
		_checkInited();
		_checkRelation(_metadata.getRelationMetaData());
		Checks.checkKey(entityKey);
		Checks.checkKey(relationKey);
		Checks.checkNull(values);
		Checks.checkEmptyValueList(values);
		MetaData related = _metadata.getRelationMetaData().getRelatedEntity();
		return _update(_metadata.getRelationMetaData(), values, _metadata.getPrimaryKey()[0].getName()+"="+entityKey+" AND "+related.getPrimaryKey()[0].getName()+"="+relationKey);
	}

	protected final int updateRelation(ValueList values, String query) throws DatabaseException {
		_checkInited();
		_checkRelation(_metadata.getRelationMetaData());
		Checks.checkNull(values);
		Checks.checkString(query);
		return _update(_metadata, values, query);
	}

	protected final int deleteRelation(long entityKey, long relationKey) throws DatabaseException {
		_checkInited();
		_checkRelation(_metadata.getRelationMetaData());
		Checks.checkKey(entityKey);
		Checks.checkKey(relationKey);
		MetaData related = _metadata.getRelationMetaData();
		return _delete(related, related.getPrimaryKey()[0].getName()+"="+entityKey+" AND "+related.getPrimaryKey()[1].getName()+"="+relationKey);
	}

	protected final int deleteRelation(long entityKey) throws DatabaseException {
		_checkInited();
		_checkRelation(_metadata.getRelationMetaData());
		Checks.checkKey(entityKey);
		return _delete(_metadata.getRelationMetaData(), _metadata.getPrimaryKey()[0].getName()+"="+entityKey);
	}

	protected final int deleteRelation(String query) throws DatabaseException {
		_checkInited();
		_checkRelation(_metadata.getRelationMetaData());
		Checks.checkString(query);
		return _delete(_metadata.getRelationMetaData(), query);
	}

	protected final void startTransaction(boolean autoRollback) throws DatabaseException {
		_checkInited();
		_connection.startTransaction(this, autoRollback);
	}

	protected final void startTransaction() throws DatabaseException {
		startTransaction(true);
	}

	protected final void commit() throws DatabaseException {
		_checkInited();
		_connection.commit(this);
	}

	protected final void rollback() throws DatabaseException {
		_checkInited();
		_connection.rollback(this);
	}
	
	protected final void checkRestriction(long entityKey, String fieldName, String value) throws RestrictionException, DatabaseException {
		checkRestriction(entityKey, fieldName, new String[] {value});
	}

	protected final void checkRestriction(long entityKey, String fieldName, String[] values) throws RestrictionException, DatabaseException {
		_checkInited();
		Checks.checkKey(entityKey);
		Checks.checkNull(values);
		Checks.checkEmptyArray(values);
		if(Strings.isEmpty(fieldName)) {
			throw new IllegalArgumentException("empty fieldValue specified");
		}
		Row result = select(fieldName, entityKey);
		if(result == null) {
			throw new IllegalArgumentException("there is no entity with entityKey "+entityKey);
		}
		else {
			boolean found = false;
			for(int i=0; i<values.length; i++) {
				if(values[i].equals(result.getString(1))) {
					found = true;
				}
			}
			if(!found) {
				throw new RestrictionException(result.getString(1));
			}
		}
	}

	// for EntityMetaData:
	// EntityMetaData needs to initialize the Entity. The constructor 
	// cannot be used to initialize these variables, in order to allow 
	// the use of an empty constructor by clients.
	void init(EntityMetaData meta, Connection c) {
		_connection = c;
		_metadata = meta;
	}
	
	private String _primaryKeyToSQLWhere(ValueList primaryKey) {
		Field[] primaryKeyFields = getMetaData().getPrimaryKey();
		// check if given fields are primary key fields:
		Map<String,Field> keys = new HashMap<String,Field>();
		for(int i=0; i<primaryKeyFields.length; i++) {
			keys.put(primaryKeyFields[i].getName(), primaryKeyFields[i]);
		}
		Iterator<?> itValues = primaryKey.iterator();
		// for the next step:
		Map<String,Value> values = new HashMap<String,Value>();
		while(itValues.hasNext()) {
			Value v = (Value)itValues.next();
			if(!keys.containsKey(v.getFieldName())) {
				throw new ValidationException("value \""+v.getFieldName()+"\" is not a primary key");
			}
			// for the next step:
			values.put(v.getFieldName(), v);
		}
		// check if all primary key fields are present:
		for(int i=0; i<primaryKeyFields.length; i++) {
			if(!values.containsKey(primaryKeyFields[i].getName())) {
				throw new ValidationException("primary key field \""+primaryKeyFields[i].getName()+"\" missing");
			}
		}
		// OK, build the where clause:
		StringBuilder sb = new StringBuilder();
		itValues = primaryKey.iterator();
		while(itValues.hasNext()) {
			Value v = (Value)itValues.next();
			sb.append(v.getFieldName());
			sb.append("=");
			sb.append(getMetaData().valueToSQL(v));
			sb.append(itValues.hasNext()? " AND " : "");
		}
		return sb.toString();
	}

	private long _insert(MetaData metaData, ValueList values, boolean returnKeys) throws DatabaseException {
		Iterator<?> itValues = values.iterator();
		// check if all required fields are given, and add default values for non-specified fields
		// 1. get a map with all the fields NOT passed
		Map<String,Field> nonExistingFields = new HashMap<String,Field>();
		nonExistingFields.putAll(metaData.getFields());
		while(itValues.hasNext()) {
			Value v = (Value)itValues.next();
			nonExistingFields.remove(v.getFieldName());
		}
		// 2. also remove the primary key field, if auto-generate-keys = true:
		if(metaData instanceof EntityMetaData) {
			EntityMetaData emd = (EntityMetaData)metaData;
			if(emd.getPrimaryKey().length==1 && emd.autoGeneratedKeys()) {
				nonExistingFields.remove(emd.getPrimaryKey()[0].getName());
			}
			// its an error if the user tries to pass in a value
			// for the primary key, when auto-generate-keys = true:
			if(emd.getPrimaryKey().length > 0) {
				itValues = values.iterator();
				while(itValues.hasNext()) {
					Value v = (Value)itValues.next();
					if(v.getFieldName().equals(emd.getPrimaryKey()[0].getName()) && emd.autoGeneratedKeys()) {
						throw new ValidationException("cannot use value \""+v.getFieldName()+"\": it's an auto-generated primary key");
					}
				}
			}
		}
		// 3. check if all required fields are passed in:
		// (this doesn't mean they're not set to null, but MetaData.valueToSQL checks that)
		Iterator<?> itNonExisting = nonExistingFields.values().iterator();
		while(itNonExisting.hasNext()) {
			Field f = (Field)itNonExisting.next();
			if(f.isRequired()) {
				throw new ValidationException("required field \""+f.getName()+"\" missing");
			}
			else {
				values.add(f.getName(), f.getDefaultValue());
			}
		}
		// build the query:
		StringBuilder sb = new StringBuilder();
		StringBuilder fieldNames = new StringBuilder();
		StringBuilder fieldValues = new StringBuilder();
		sb.append("INSERT INTO ");
		sb.append(metaData.getName());
		sb.append(" (");
		itValues = values.iterator();
		while(itValues.hasNext()) {
			Value v = (Value)itValues.next();
			fieldNames.append(v.getFieldName());
			fieldNames.append(itValues.hasNext() ? ", " : "");
			fieldValues.append(metaData.valueToSQL(v));
			fieldValues.append(itValues.hasNext() ? ", " : "");
		}
		sb.append(fieldNames.toString());
		sb.append(") VALUES (");
		sb.append(fieldValues.toString());
		sb.append(")");
		return _connection.performQuery(sb.toString(), this, returnKeys, values);
	}

	private int _update(MetaData metaData, ValueList values, String query) throws DatabaseException {
		StringBuilder sb = new StringBuilder();
		sb.append("UPDATE ");
		sb.append(metaData.getName());
		sb.append(" SET ");
		Iterator<?> itValues = values.iterator();
		while(itValues.hasNext()) {
			Value v = (Value)itValues.next();
			// validate values:
			sb.append(v.getFieldName());
			sb.append("=");
			sb.append(metaData.valueToSQL(v));
			sb.append(itValues.hasNext() ? ", " : "");
		}
		if(!Strings.isEmpty(query)) {
			sb.append(" WHERE "+query);
		}
		return (int)_connection.performQuery(sb.toString(), this, false, values);
	}

	private int _delete(MetaData metaData, String query) throws DatabaseException {
		if(Strings.isEmpty(query)) {
			return (int)_connection.performQuery("DELETE FROM "+metaData.getName(), this, false, null);
		}
		else {
			return (int)_connection.performQuery("DELETE FROM "+metaData.getName()+" WHERE "+query, this, false, null);
		}
	}

	private void _checkInited() {
		if(_connection == null || _metadata == null) {
			throw new IllegalStateException("entity has not been properly initialized");
		}
	}
	
	private void _checkRelation(RelationMetaData rm) {
		if(rm == null) {
			throw new UnsupportedOperationException("entity \""+_metadata.getName()+"\" does not have a relation");
		}
	}

	//TODO reuse toolset.Checks
	private static class Checks {

		public static void checkKey(long key) {
			if(key <= 0) {
				throw new IllegalArgumentException("illegal key value: "+key);
			}
		}
		
		public static void checkNull(Object o) {
			if(o == null) {
				throw new IllegalArgumentException("null argument");
			}
		}

		public static void checkEmptyArray(Object[] array) {
			if(array.length == 0) {
				throw new IllegalArgumentException("empty argument");
			}
		}

		public static void checkEmptyValueList(ValueList values) {
			if(values.size() == 0) {
				throw new IllegalArgumentException("no values specified");
			}
		}
		
		public static void checkString(String query) {
			if(Strings.isEmpty(query)) {
				throw new IllegalArgumentException("no query specified");
			}
		}
	}
	
}
